<?php

namespace slinks\core\dependencyInjectionExt\compiler;

use slinks\core\DependencyInjection\ContainerInterface;
use slinks\core\DependencyInjection\Definition;
use slinks\core\DependencyInjection\ContainerBuilder;
use slinks\core\dependencyInjectionExt\Reference;
use slinks\core\DependencyInjection\Compiler\PassConfig;

/**
 * Autowires Services with Interface/Class type parameter matching,
 * 
 * @author Navid Mitchell
 *
 */
class ResolveInterfaceReferencesPass implements ICompilerPass {

    /**
     * You can modify the container here before it is dumped to PHP code.
     *
     * @param ContainerBuilder $container
     * @return void
     */
    public function process(ContainerBuilder $container) {
        foreach ($container->getDefinitions() as $serviceId => $definition) {
            if ($definition->isSynthetic()) {
                continue;
            }

            $definition->setArguments(
                    $this->processArguments($container,$serviceId,$definition->getArguments())
            );

            $calls = array();
            foreach ($definition->getMethodCalls() as $call) {

                $arguments = $this->processArguments($container,$serviceId,$call[1]);
                if ($arguments !== null) {
                    $calls[] = array($call[0], $arguments);
                }
            }
            $definition->setMethodCalls($calls);
        }
    }

    protected function processArguments(ContainerBuilder $container,$serviceId, array $arguments) {
        $ret = array();
        foreach ($arguments as $k => $argument) {
            if (is_array($argument)) {
                // if this returns null then the argument was in error so also return null.
                // this will cause any invalid argument to bubble null all the way up the call stack.  
                $val = $this->processArguments($container,$serviceId,$argument);
                if ($val == null) {
                    $ret == null;
                    break;
                }
                $ret[$k] = $val;
            } else if ($argument instanceof Reference && $argument->isInterface()) {
                // id can be an interface name or a service id
                $id = (string) $argument;

                $exists = false;
                $multipleMatches = false;    
                // if the interface is an instance of the container we just manual add since the container will always be referenced by slinks.container
                // and it is not a tagged definition at this point
                if($id == 'slinks\\core\\DependencyInjection\\ContainerInterface' 
                        || $id == 'slinks\\core\\DependencyInjection\\Container'
                        || $id == 'slinks\\core\\dependencyInjectionExt\\ContainerExt'){
                    $id = 'slinks.container';
                    $exists = true;
                }else{
                    // find any services that were tagged with interface/class wanted.
                    $definitionIds = array_keys($container->findTaggedServiceIds($id));
                    if (count($definitionIds) > 1) {
                        // there is more than 1 service with the interface
                        // but we have a qualifier so lets resolve 
                        $qualifier = $argument->getQualifier();
                        if ($qualifier != null) {
                            foreach ($definitionIds as $definitionId) {
                                $definition = $container->getDefinition($definitionId);
                                $exists = $this->hasMatchingQualifier($qualifier, $definition);
                                if ($exists) {
                                    $id = $definitionId;
                                    break;
                                }
                            }
                        }
                        $multipleMatches = true;
                    } else if (count($definitionIds) > 0) {
                        $exists = true;
                        $id = $definitionIds[0];
                    }
                }
                
                if ($exists) {
                    $ret[$k] = new Reference($id);
                } else if (!$exists && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE == $argument->getInvalidBehavior()) {
                    throw new \LogicException('Service '.$serviceId.' Argument Reference:' . $id . ($multipleMatches ? ' ambiguous multiple mathces found and no qualifier given.' : ' interface not implemented by any service.'));
                } else if (!$exists && ContainerInterface::NULL_ON_INVALID_REFERENCE === $argument->getInvalidBehavior()) {
                    $ret[$k] = null;
                } else if (!$exists && ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $argument->getInvalidBehavior()) {
                    $ret = null;
                    break;
                }
                
            } else {
                $ret[$k] = $argument;
            }
        }

        return $ret;
    }

    private function hasMatchingQualifier($qualifier, Definition $definition) {
        if ($definition->hasTag('qualifier')) {
            $qualifiers = $definition->getTag('qualifier');
            foreach ($qualifiers as $value) {
                if ($qualifier === $value) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * @return the compiler pass type. Such as PassConfig::TYPE_BEFORE_OPTIMIZATION
     */
    public function getType() {
        return PassConfig::TYPE_BEFORE_OPTIMIZATION;
    }

}